package com.example.securitydemo.rest;

import com.example.securitydemo.config.AppProperties;
import com.example.securitydemo.domain.Auth;
import com.example.securitydemo.domain.MfaType;
import com.example.securitydemo.domain.User;
import com.example.securitydemo.domain.dto.LoginDto;
import com.example.securitydemo.domain.dto.SendTotpDto;
import com.example.securitydemo.domain.dto.TotpVerificationDto;
import com.example.securitydemo.domain.dto.UserDto;
import com.example.securitydemo.exception.*;
import com.example.securitydemo.service.EmailService;
import com.example.securitydemo.service.SmsService;
import com.example.securitydemo.service.UserCacheService;
import com.example.securitydemo.service.UserService;
import com.example.securitydemo.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.context.MessageSource;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Locale;
import java.util.Optional;

/**
 * Created by Guaguaerhao on 2021/4/15 23:50
 */
@Slf4j
@RequestMapping("/authorize")
@RestController
@RequiredArgsConstructor
public class AuthorizeResource {

    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final AppProperties appProperties;
    private final UserCacheService userCacheService;
    private final SmsService smsService;
    private final EmailService emailService;


    @PostMapping("/register")
    public void register(@Valid @RequestBody UserDto userDto, Locale locale) {
        // TODO: 1. 检查 username、email、mobile 都是唯一的，所以要查询数据库确保唯一
        if (userService.isUsernameExisted(userDto.getUsername())) {
            throw new DuplicateProblem("用户名重复");
        }
        if (userService.isEmailExisted(userDto.getEmail())) {
            throw new DuplicateProblem("电子邮箱地址重复");
        }
        if (userService.isMobileExisted(userDto.getMobile())) {
            throw new DuplicateProblem("手机号码重复");
        }
        // TODO: 2. 我们需要 userDto 转成 User, 我们给一个默认角色（ROLE_USER），然后保存
        User user = User.builder()
                .username(userDto.getUsername())
                .name(userDto.getName())
                .email(userDto.getEmail())
                .mobile(userDto.getMobile())
                .password(userDto.getPassword())
                .build();

        userService.register(user);
    }

    /**
     * 获取 token
     * @param loginDto
     * @return
     */
    @PostMapping("/token")
    public ResponseEntity<?> login(@RequestBody LoginDto loginDto) {
        return userService.findOptionalByUsernameAndPassword(loginDto.getUsername(), loginDto.getPassword())
                .map(user -> {
                    // 1.升级密码编码
                    userService.upgradePasswordEncodingIfNeeded(user, loginDto.getPassword());
                    // 2.验证
                    if (!user.isEnabled()) {
                        throw new UserNotEnabledProblem();
                    }
                    if (!user.isAccountNonLocked()) {
                        throw new UserAccountLockedProblem();
                    }
                    if (!user.isAccountNonExpired()) {
                        throw new UserAccountExpiredProblem();
                    }
                    // 3.判断 usingMfa 如果是 false，我们就直接返回 Token
                    if (!user.isUsingMfa()) {
                        return ResponseEntity.ok().body(userService.login(loginDto.getUsername(), loginDto.getPassword()));
                    }
                    // 4.使用多因子认证
                    val mfaId = userCacheService.cacheUser(user);
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                            .header("X-Authenticate", "mfa", "realm=" + mfaId)
                            .build();
                })
                .orElseThrow(() -> new BadCredentialsException("账号或密码错误"));
    }

    @PutMapping("/totp")
    public void sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto) {
        userCacheService.retrieveUser(sendTotpDto.getMfaId())
                .flatMap(user -> userService.createTotp(user).map(code -> Pair.of(user, code)))
                .ifPresentOrElse(userStringPair -> {
                    log.debug("totp: {}",  userStringPair.getSecond());
                    if (sendTotpDto.getMfaType() == MfaType.SMS) {
                        smsService.send(userStringPair.getFirst().getMobile(), userStringPair.getSecond());
                    } else if(sendTotpDto.getMfaType() == MfaType.EMAIL) {
                        emailService.send(userStringPair.getFirst().getEmail(), userStringPair.getSecond());
                    }
                }, () -> new InvalidTotpProblem());
    }

    @PostMapping("/totp")
    public Auth verifyTotp(@Valid @RequestBody TotpVerificationDto totpVerificationDto) {
        return userCacheService.verifyTotp(totpVerificationDto.getMfaId(), totpVerificationDto.getCode())
                .map(User::getUsername)
                .flatMap(userService::findOptionalByUsername)
                .map(userService::loginWithTotp)
                .orElseThrow(InvalidTotpProblem::new);
    }

    @GetMapping("/token/refresh")
    public Auth refreshToken(@RequestHeader(name = "Authorization") String accessToken,
                             @RequestParam String refreshToken) {
        accessToken = accessToken.replace(appProperties.getJwt().getPrefix(), "");
        if (jwtUtil.validateAccessTokenWithoutExpired(accessToken) && jwtUtil.validateRefreshToken(refreshToken)) {
            return new Auth(
                    jwtUtil.buildAccessTokenWithRefreshToken(refreshToken),
                    refreshToken
            );
        }
        throw new AccessDeniedException("Bad Credentials");
    }
}
